package giuliani_mapelli_marocco.business;

import giuliani_mapelli_marocco.entity.Event;
import giuliani_mapelli_marocco.entity.Weatherforecast;
import giuliani_mapelli_marocco.entity.WeatherforecastPK;
import giuliani_mapelli_marocco.enumerations.WeatherMessageType;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.ejb.Asynchronous;
import javax.ejb.EJB;
import javax.ejb.EJBException;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import net.aksingh.java.api.owm.CurrentWeatherData;
import net.aksingh.java.api.owm.DailyForecastData;
import net.aksingh.java.api.owm.OpenWeatherMap;
import org.json.JSONException;

@Stateless
public class WeatherManager {
    
    @PersistenceContext
    private EntityManager em;
    @EJB
    private WeatherNotificationManager wnm;
   
    private TimeTools time = new TimeTools();
    private OpenWeatherMap owm = new OpenWeatherMap("6e279645c916f6f906b20e20a195dbb2");;
    private Weatherforecast weather;
    
    public WeatherManager() {
    }
    
    /**
     * This method will be called by the EventBean during the phase of creation of event and it will create a weather 
     * istance, for every days between the interval of time selected, if not present yet in the DB.
     * @param place the place in wich the event will be helded
     * @param startDate the starting date of the event
     * @param endDate the ending date of the event
     * @throws IOException  
     */
    @Asynchronous
    public void createWeatherInfo(String place, Date startDate, Date endDate, Event event) throws IOException {
        //Check if the weather info are already in the DB
        //if yes: I receive the list of date that are missing for that particular place
        //if no: a null result is received and we will pick up all the necessary dates from the weatherAPI
        List<Date> dbDates = this.datesNotInYet(place, startDate, endDate);
        List<DailyForecastData.Forecast> forecast_List = retrieveWeatherByPlace(place);
        if(forecast_List!=null) {
                if(dbDates==null) {
                    List<Date> intervalOfTime = time.getDateList(startDate, endDate);
                    this.persistTheInfo(intervalOfTime, forecast_List, place, event, startDate, endDate);
                } else {
                    this.persistTheInfo(dbDates, forecast_List, place, event, startDate, endDate);
                }
        }
    }
 
    /**
     * Support method that is called by createWeatherInfo(String place, Date startDate, Date endDate) to store actually
     * the info in the DB.
     * @param date List of dates, wich corresponds to a weather forecast on the DB
     * @param forecast List of weather forecasts retrieved via web
     * @param place the place of wich the forecast are referred to
     */
    private void persistTheInfo(List<Date> missingDate, List<DailyForecastData.Forecast> forecast, String place, Event event, Date start, Date end) {
         
        completeEventWeatherTable(event, missingDate, start, end, place);
      
        for(Date d : missingDate) {
            for(DailyForecastData.Forecast f : forecast) {
                Date weatherData = time.changeDateFormat(f.getDateTime());
                if(d.equals(weatherData)) {
                    weather = new Weatherforecast();
                    WeatherforecastPK pk = new WeatherforecastPK();
                    weather.setWeatherforecastPK(pk);
                    weather.getWeatherforecastPK().setDate(weatherData);
                    weather.getWeatherforecastPK().setPlace(place);
                    weather.setTempMax((float) this.convertTemperature(f.getTemperature_Object().getMaximumTemperature()));
                    weather.setTempMin((float) this.convertTemperature(f.getTemperature_Object().getMinimumTemperature()));
                    weather.setHumidity((int)f.getHumidity());
                    List<DailyForecastData.Forecast.Weather> weather_List = f.getWeather_List();
                    for (DailyForecastData.Forecast.Weather weather_List1 : weather_List) {
                        weather.setWeather(weather_List1.getWeatherCode());

                        List<Event> list =(List<Event>) weather.getEventCollection();
                        if(list == null) {
                            list = new ArrayList();
                        }
                        list.add(event);
                        weather.setEventCollection(list);
                        em.persist(weather);
                    }
                    break;
                }               
            }
        }                        
    }
    
    /**
     * This method will populate the JoinTable eventweather in the DB, for the relationship between event and weatherforecast
     * @param event the event to link in the table with the weatherforecast
     * @param missingDates the dates that are to add to the JoinTable because there are already existent weatherforecast in the DB
     * @param start the date in wich starts the event
     * @param end the ending date of the event
     * @param place the place in wich the event take place
     */
    private void completeEventWeatherTable(Event event, List<Date> missingDates, Date start, Date end, String place) {
         
        List<Date> alreadyExistentDates = missingDates;//The dates that are missing from the DB
        List<Date> dateFromInput = time.getDateList(start,end);//All the dates that are given by the input 
        dateFromInput.removeAll(alreadyExistentDates);
        for(Date dateToAdd : dateFromInput) {   
            Weatherforecast w =(Weatherforecast) em.createNamedQuery("Weatherforecast.findByDateAndPlace")
                    .setParameter("place", place).setParameter("date", dateToAdd).getSingleResult();
            List<Event> list =(List<Event>) w.getEventCollection();
            list.add(event);
            w.setEventCollection(list);
            em.merge(w);
        } 
    }
    
    /**
     * This private method is called to create a List of date for a particular place not already present in the DB, 
     * which are required
     * @param place the place for the weather forecast
     * @param start the starting time of the event 
     * @param end the ending date of the event 
     * @return List<Date> of the interested dates, to use to retrieve info about the weather forecast
     */
    private List<Date> datesNotInYet(String place, Date start, Date end) {
        try {
            List<Weatherforecast> w =(List<Weatherforecast>) em.createNamedQuery("Weatherforecast.findByPlace").setParameter("place", place).getResultList();
            //Creating list of dates already existent
            List<Date> dates = new ArrayList();
            //Retrieving the list of dates from the the parameter passed via function
            List<Date> input = time.getDateList(start,end);
            //List of Dates to return 
            List<Date> result = new ArrayList();
            //Filling the dates list with the dates actually present in the DB 
            for(Weatherforecast w1 : w) {
                dates.add(w1.getWeatherforecastPK().getDate());   
            } 
            //populating the last list with the dates not already present in the DB for that specific location
            for(Date d2: input) {
                boolean equals=false;
                for(Date d1: dates) {   
                    if((d1.equals(d2))) {
                        equals=true;
                    }
                }
                if(!equals)
                result.add(d2);
            }
            //Is built the list of Dates that needs forecast for that particular place
            return result;
           
        } catch(EJBException e) {
            return null;
        } catch(ClassCastException e) {
            return null;
        }
    }
           
    /**
     * This method will delete the weather forecast info found by date (is also updated the join table eventweather)
     * @param date date of the weather forecast to find in the DB
     */
    public void deleteWeatherForecastByDate(Date date) {
        List<Weatherforecast> forecast = em.createNamedQuery("Weatherforecast.findByDate").setParameter("date", date).getResultList();
        for(Weatherforecast f: forecast) {
           em.remove(f);
           em.flush();
        }
    }
    
    /**
     * This method will eliminate the previous weatherCollection associated to the specified event, updating also the 
     * eventweather table.
     * @param e  the event from wich the collection has to be removed
     */
    public void deleteFromEventWeatherTable(Event e) {
        List<Weatherforecast> collectionWeatherforecast = getWeatherByIdEvent(e.getId());
        for(Weatherforecast w: collectionWeatherforecast) {
            List<Event> collectionEvent = (List<Event>)w.getEventCollection();
            collectionEvent.remove(e);
            w.setEventCollection(collectionEvent);
            em.merge(w);
            em.flush();
        }
    }
    
   /**
    * This method will be called periodically to update all the weather info present in the DB, 
    * starting from the current date up to 16 days.
    */
    public void updateAllWeatherforecast() {
        Date currentDate = time.today();
        
        List<String> places = (List<String>) em.createQuery("SELECT distinct w.weatherforecastPK.place FROM Weatherforecast w").getResultList();
        
            for(String p: places) {
             List<Weatherforecast> weatherToUpdate = em.createQuery("SELECT w FROM Weatherforecast w"
                + " WHERE (w.weatherforecastPK.date>= :currentDate AND w.weatherforecastPK.date <= :lastDate AND w.weatherforecastPK.place =:place)").setParameter("currentDate", currentDate)
                .setParameter("lastDate", time.addDays(currentDate, 16)).setParameter("place", p).getResultList(); 
             
            try {
                updateWeather(weatherToUpdate, p);
            } catch (IOException ex) {
                Logger.getLogger(WeatherManager.class.getName()).log(Level.SEVERE, null, ex);
            }
            
            }
        }
                
    /**
     * This function will be called by updateAllWeatherForecast() to retrieve the correct dates from the correct place
     * from the weatherAPI
     * @param date List of Dates that need to be updated
     * @param place in which we have to retrieve the weatherInfo
     * @throws IOException 
     */
    private void updateWeather(List<Weatherforecast> weathers, String place) throws IOException {
        
       List<DailyForecastData.Forecast> forecast_List = retrieveWeatherByPlace(place);
                 for(Weatherforecast w : weathers) {
                         for(DailyForecastData.Forecast f : forecast_List) {
                         Date weatherData = time.changeDateFormat(f.getDateTime());
                                    if(w.getWeatherforecastPK().getDate().equals(weatherData)) {
                                    int weatherCode = w.getWeather();//Save the previous weathercode
                                    w.setTempMax((float) this.convertTemperature(f.getTemperature_Object().getMaximumTemperature()));
                                    w.setTempMin((float) this.convertTemperature(f.getTemperature_Object().getMinimumTemperature()));
                                    w.setHumidity((int)f.getHumidity());
                                    List<DailyForecastData.Forecast.Weather> weather_List = f.getWeather_List();
                                    for (DailyForecastData.Forecast.Weather weather_List1 : weather_List) {
                                    w.setWeather(weather_List1.getWeatherCode());
                                       
                                        if(weatherCode!=w.getWeather()) { 
                                        createWeatherNotification(w);
                                        //Create a notification if the weatherchanges;
                                        }
                                        
                                        em.merge(w);
                                        em.flush();
                                        
                                    }
                                    break;
                                }               
            }
                 }
    }
    
    
    /**
     * This method will return the List of Weatherforecast using the event id
     * @param id the event id
     * @return  The List of weatherforecast associated to a particular event
     */
    public List<Weatherforecast> getWeatherByIdEvent(Long id) {
        
        List<Weatherforecast> weatherInfo =  em.createQuery("SELECT w FROM Weatherforecast w JOIN w.eventCollection e WHERE (e.id = :id AND e.deleted =:deleted)")
                .setParameter("id", id).setParameter("deleted", false).getResultList();
        
        return weatherInfo;
        
    }
    
    /**
     * This method will call the WeatherNotificationManager method to create a specific kind of event notification
     * only associated with aoutdoor event forecast
     * @param forecast the weather forecast associated with the particular notification to create.
     */
    private void createWeatherNotification(Weatherforecast forecast) {
        
        System.out.println("Creo la notifica meteo");
        List<Event> outdoor = new ArrayList();
        List<Event> events = (List<Event>) forecast.getEventCollection();
        for(Event e: events) {
            if(e.getOutdoor() && e.getDeleted()==false) {
                outdoor.add(e);
            }
        }
        if(outdoor!=null) {
        for(Event e1: outdoor) {
            wnm.createNotificationWeather(e1, WeatherMessageType.WHEATHER_CHANGED);
        }
    
    }
    }
    
    /**
     * This method will give a date to suggest to the organizer of the event in case of bad weather conditions,
     * if the event is composed by more days, this methos will verify if the overall near to the date that it is proposed
     * are overall good (if they are bad the date will not be proposed)
     * @param e the event that we will take in consideration
     * @return the first Date available with good weather
     * @throws IOException 
     */
    public Date calculateTheClosestSunnyDay(Event e) throws IOException {
        
        List<DailyForecastData.Forecast> forecast = retrieveWeatherByPlace(e.getPlace());
        long eventDuration = time.daysDiff(e.getStartDate(), e.getEndDate());
        List<DailyForecastData.Forecast> updatedForecast = new ArrayList();
        //Populate the list with only date after the starting date of the event
        for(DailyForecastData.Forecast f : forecast) {
                Date weatherDate = time.changeDateFormat(f.getDateTime());
                if(weatherDate.after(e.getStartDate())) {
                    updatedForecast.add(f);
                    System.out.println(f.getDateTime());
                }
        }
        //I read the list and find a day with overall good weather for all the duration of the event with the correct list 
        //with the correct dates
        for(DailyForecastData.Forecast f : updatedForecast) {
             int daysCounter = 1;
             int goodDays = 0;
             int badDays = 0;
             Date dateF = time.changeDateFormat(f.getDateTime());
             List<DailyForecastData.Forecast.Weather> weather_List = f.getWeather_List();
                                    
                                    int code = weather_List.get(0).getWeatherCode();
                                     
                                        if(checkIfTheWeatherIsBad(code)) {
                                            badDays++;
                                        } else {
                                            goodDays++;
                                        }
                                    
                                   while (daysCounter<eventDuration) {
                                     Date date = time.addDays(dateF, daysCounter);
                                     //Begin the internal loop to count the good or bad days
                                     for(DailyForecastData.Forecast f1 : forecast) {
                                         if(date.equals(time.changeDateFormat(f1.getDateTime()))) {
                                             List<DailyForecastData.Forecast.Weather> weather_List1 = f1.getWeather_List(); 
                                             int code1 = weather_List1.get(0).getWeatherCode();
                                             if(checkIfTheWeatherIsBad(code1)) {
                                                   badDays++;
                                             } else {
                                                   goodDays++;
                                             }
                                             break;
                                         } //Vedere se bisogna fare qualcosa per il caso limite se arriviamo alla fine dei 16 giorni
                                     }
                                     daysCounter++;
                                }
                                   //Check if overall si bad or good weather
                                   if(goodDays>=badDays) {
                                       return dateF;
                                   }
        }
         return null;   //No dates were found                        
                                   
    }
    
    /**
     * method that examine the weatherCode and provide the info about the status of the weather(Good or bad)
     * @param code weather code passed to be examined
     * @return true if the weather is bad, false otherwise
     */
    private boolean checkIfTheWeatherIsBad(int code) {
        
         switch(code/100) {
             
             case 8:
                 return false;
             
             case 9:
                 if(code>=951 && code<=956) {
                     return false;
                 } else {
                     return true;
                 }
         }
        return true;
    }
    
    /**
     * This method will retrieve the weatherInfo for a particular place (if is available)
     * @param place the place form wich we want to retrieve the info
     * @return the list of weatherforecast associated from the web API.
     * @throws IOException 
     */
    private List<DailyForecastData.Forecast> retrieveWeatherByPlace(String place) throws IOException {
        
        byte days = 16;
        CurrentWeatherData cwd; 
        try {
            cwd = owm.currentWeatherByCityName(place);
            if(cwd.hasCityName()) {
                //List of everyPrevision from today up to 16 days.
                String city = cwd.getCityName();
                List<DailyForecastData.Forecast> forecast_List = owm.dailyForecastByCityName(city, days).getForecast_List();
                return forecast_List;
            }
                } catch (MalformedURLException ex) {
                    Logger.getLogger(WeatherManager.class.getName()).log(Level.SEVERE, null, ex);
                } catch (JSONException ex) {
                    Logger.getLogger(WeatherManager.class.getName()).log(Level.SEVERE, null, ex);
        }
              return null;  
     }
     
    /**
     * This method provides conversion between F to C
     * @param temperature the temperature to convert
     * @return the temperature converted as a double
     */
    private double convertTemperature(double temperature) {
        double celsius = (double)(temperature-32)/1.80000; 
        return celsius;
    }  
 }